﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using ChromaCode.Models;
using System.Windows.Documents;
using ChromaCode.Extensions;

namespace ChromaCode.Services
{
    /// <summary>
    /// Service for applying a set of group rules to a range of text
    /// </summary>
    internal class GroupRuleService : RuleBaseService<GroupRule>
    {
        #region Public

        /// <summary>
        /// Initialise the GroupRuleService and start the process of working out which areas of the paragraph to highlight
        /// </summary>
        /// <param name="rules">A list of rules to use</param>
        /// <param name="startParagrph">The paragraph we want to start highlighting blocks at</param>
        /// <param name="highlightWholeDoc">If set to try then we are refreshing the whole document this will alter the
        /// way certain processes work</param>
        public GroupRuleService(List<Rule> rules, Paragraph startParagrph, bool highlightWholeDoc)
        {
            if (rules != null)
            {
                StartParagraph = startParagrph;
                HighlightWholeDoc = highlightWholeDoc;
                m_AllRules = rules;

                // Get any group rules from the supplied list of rules
                Rules = rules.Where(x => x.TypeOfRule == RuleType.GroupHighlighter).Select(y => (GroupRule)y).ToList();

                // If we have rules and a text range then start the highlight process
                if ((Rules != null) && (Rules.Count > 0) && (StartParagraph != null))
                {
                    DoHighlighting();
                }
            }
        }

        #endregion Public

        #region Private

        /// <summary>
        /// Main function that carries out the highlight process
        /// </summary>
        private void DoHighlighting()
        {

            Paragraph currentParagraph = StartParagraph;
            int startPos = 0;

            bool currentParagraphOpen = currentParagraph.Tag != null;

            // Get the text range for the current paragraph and clear all its properties
            TextRange tr = new TextRange(currentParagraph.ContentStart, currentParagraph.ContentEnd);
            tr.ClearAllProperties();

            // Do we have an open rule from the previous paragraph that we need to try and close
            GroupRule openRule = GetPreviousParagraphOpenRule(currentParagraph);

            // Loop for ever and ever and ever and ..........
            for (; ; )
            {
                if (openRule == null)
                {
                    // No rule is open on the current paragraph so make sure the tag is null
                    currentParagraph.Tag = null;

                    // No current open rule so try and open a new one in this paragraph
                    openRule = OpenRule(ref startPos, tr);

                    if (openRule == null)
                    {
                        // If openRule is still null then no more rules need to be processed on this paragraph. But if the
                        // current paragraph originally had an open rule on it then we must check the next paragraph as it
                        // may have been affected by us closing a rule on the current paragraph that was once open.
                        //
                        // However, we only need to do this if we are not refreshing the whole document as i fwe are doing
                        // the whole document then all the paragraphs will be refreshed is any case
                        if ((currentParagraphOpen) && (!HighlightWholeDoc))
                        {
                            currentParagraph = GetNextParagraph(currentParagraph);
                            if (currentParagraph != null)
                            {
                                // First refresh all other highlighting on the next paragraph
                                RefreshParagraphHighlighting(currentParagraph);

                                tr = new TextRange(currentParagraph.ContentStart, currentParagraph.ContentEnd);
                                tr.ClearAllProperties();
                                startPos = 0;
                                currentParagraphOpen = currentParagraph.Tag != null;
                                currentParagraph.Tag = null;

                                // Now this paragraph is no longer under the previous paragraphs rull set, see if it
                                // has any rules
                                openRule = OpenRule(ref startPos, tr);
                            }
                            else
                            {
                                // No more paragraphs so stop
                                break;
                            }
                        }
                        else
                        {
                            // No more rules and the current paragraph did not have an open rule on it so stop
                            break;
                        }
                    }
                }

                if (openRule != null)
                {
                    // We have an open rule so try and close it
                    bool ruleClosed;
                    startPos = CloseRule(openRule, tr, startPos, out ruleClosed);

                    if (!ruleClosed)
                    {
                        // The rule wasn't closed so assign it to the paragraph so we know that this paragraph has an open rule
                        currentParagraph.Tag = openRule;

                        // Move on to the next paragraph as we will try and close the rule on the next paragraph the next time
                        // through the loop
                        currentParagraph = GetNextParagraph(currentParagraph);

                        // Get the text range for the next paragraph and clear all its properties
                        if (currentParagraph != null)
                        {
                            tr = new TextRange(currentParagraph.ContentStart, currentParagraph.ContentEnd);
                            tr.ClearAllProperties();
                            startPos = 0;

                            // Store whether the current paragraph has an open rule
                            currentParagraphOpen = (currentParagraph.Tag != null);
                        }
                        else
                        {
                            // No more paragraphs so stop here
                            break;
                        }
                    }
                    else
                    {
                        // no open rules on this paragraph so far
                        currentParagraph.Tag = null;
                        openRule = null;
                    }
                }
            }
        }

        /// <summary>
        /// Try and close the supplied rule from the sart point in the range to the end of the range
        /// </summary>
        /// <param name="ruleToClose">The rule that we want to try and close</param>
        /// <param name="range">The range we are trying to close the rule in</param>
        /// <param name="startPos">The position in the paragraph that we are starting at</param>
        /// <param name="ruleClosed">True if the rule was closed, otherwise false</param>
        /// <returns>The end position of the rule that we closed (or -1 if we reached the end of the paragraph without closing)</returns>
        private int CloseRule(GroupRule ruleToClose, TextRange range, int startPos, out bool ruleClosed)
        {
            // Assume there is no end point and the rule is not closed
            int endPos = -1;
            ruleClosed = false;

            if (range != null)
            {
                string text = range.Text;

                if (startPos <= text.Length - 1)
                {
                    // The start position is within the paragraphs text so get the posistion of the rules "end string"
                    if (string.IsNullOrEmpty(ruleToClose.EndString))
                    {
                        // No end string specified so set the end position to the end of the text range
                        endPos = text.Length;
                    }
                    else
                    {
                        // We have an end string so hunt for it
                        endPos = text.IndexOf(ruleToClose.EndString, startPos + 1);
                    }

                    // Remember to add the length of the closing "end string" to the end position or it won't get highlighted 
                    // correctly. Only do this if we have an end position though
                    if (endPos >= 0)
                    {
                        endPos = endPos + ruleToClose.EndString.Length;
                    }

                    // Get the start and end pointers for this rule
                    TextPointer startPointer = range.Start.GetInsertationPointAtOffset(startPos);
                    TextPointer endPointer = (endPos >= 0) ? range.Start.GetInsertationPointAtOffset(endPos) :
                        range.End;

                    // Create a new highlight element and add it to the list
                    HighlightElement element = new HighlightElement(startPointer, endPointer, ruleToClose);
                    HighlightElements.Add(element);

                    // Did we close the rule
                    ruleClosed = endPos >= 0;
                }
            }

            return endPos;
        }

        /// <summary>
        /// Open a group rule by finding the next rule in m_GroupRules from the start position of the range whose StartString
        /// is contained within the range
        /// </summary>
        /// <param name="startPos">The start position. This is a reference paramter as it is updated to the start posistion
        /// of the next open rule, if one is found</param>
        /// <param name="range">The test range to scan</param>
        /// <returns>A group rule to open, or null if no rules were found</returns>
        private GroupRule OpenRule(ref int startPos, TextRange range)
        {
            GroupRule result = null;
            string text = range.Text;
            int rulePosInText = startPos;
            int nextRulePos = text.Length - 1;

            foreach (GroupRule rule in Rules)
            {
                rulePosInText = text.IndexOf(rule.StartString, startPos);
                if ((rulePosInText >= 0) && (rulePosInText <= nextRulePos))
                {
                    result = rule;
                    nextRulePos = rulePosInText;
                }
            }

            if (result != null)
            {
                // We have a open rule so update the start position
                startPos = nextRulePos;
            }

            return result;
        }

        /// <summary>
        /// Get the group rule (if any) from the previous paragraph. An open group rule is a group that was not closed
        /// off before the end of the paragraph, so we need to see if it is closed off in this paragraph
        /// </summary>
        /// <param name="currentParagraph">The current paragraph that we are highlighting</param>
        /// <returns>Am open group rule if one was found, otherwise null</returns>
        private GroupRule GetPreviousParagraphOpenRule(Paragraph currentParagraph)
        {
            Paragraph prevParagraph = GetPreviousParagraph(currentParagraph);

            // Get the open rule (if any) from the previous paragraphs tab proeprty
            GroupRule openRule = null;
            if ((prevParagraph != null) && (prevParagraph.Tag is GroupRule))
            {
                openRule = (GroupRule)prevParagraph.Tag;
            }

            return openRule;
        }

        /// <summary>
        /// Get the next paragraph on from the current paragraph that is supplied
        /// </summary>
        /// <param name="currentParagraph">The current paragraph that we are highligting</param>
        /// <returns>The next paragraph or null if the current paragraph is the last one in the document</returns>
        private Paragraph GetNextParagraph(Paragraph currentParagraph)
        {
            // Try and get the start of the next paragraph
            TextPointer nextParagraphStart = currentParagraph.ContentEnd.GetNextInsertionPosition(LogicalDirection.Forward);

            Paragraph nextParagraph = null;
            if (nextParagraphStart != null)
            {
                nextParagraph = nextParagraphStart.Paragraph;
            }

            return nextParagraph;
        }

        /// <summary>
        /// Get the previous paragraph to the current paragraph that is supplied
        /// </summary>
        /// <param name="currentParagraph">The current paragraph that we are highlighting</param>
        /// <returns>The previous paragraph or null if the current paragraph is the first one in the document</returns>
        private Paragraph GetPreviousParagraph(Paragraph currentParagraph)
        {
            // Try and get the end of the previous paragraph
            TextPointer prevParagraphEnd = currentParagraph.ContentStart.GetNextInsertionPosition(LogicalDirection.Backward);

            Paragraph prevParagraph = null;
            if (prevParagraphEnd != null)
            {
                prevParagraph = prevParagraphEnd.Paragraph;
            }

            return prevParagraph;
        }

        /// <summary>
        /// Refresh the character, word and reg expression highlighting for the supplied paragraph. This is needed as if
        /// editing a group causes other paragraphs to be affected then we need to make sure that all highlighting of these
        /// other paragraphs is fully up to date.
        /// </summary>
        /// <param name="paragraphToHighlight">The paragraph whoes highlighting will be refreshed</param>
        private void RefreshParagraphHighlighting(Paragraph paragraphToHighlight)
        {
            // Get character highlight elements
            CharRuleService charService = new CharRuleService(m_AllRules, paragraphToHighlight);
            HighlightElements.AddRange(charService.HighlightElements);
            charService = null;

            // Get word highlight elements
            WordRuleService wordService = new WordRuleService(m_AllRules, paragraphToHighlight);
            HighlightElements.AddRange(wordService.HighlightElements);
            wordService = null;

            // Get regular expression highlight elements
            RegExRuleService regExpService = new RegExRuleService(m_AllRules, paragraphToHighlight);
            HighlightElements.AddRange(regExpService.HighlightElements);
            regExpService = null;
        }

        /// <summary>
        /// A list of all rules supplied to the group rule service
        /// </summary>
        private List<Rule> m_AllRules;

        #endregion Private
    }
}
